/**
 * 
 */
package clientCommunication;

import infrastructure.CommunicationCustomException;
import infrastructure.KVMessageImp;
import infrastructure.LoggingManager;
import infrastructure.MarshallerKvMessage;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.HashSet;
import java.util.Set;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;

import org.apache.log4j.Logger;

import common.messages.KVMessage;
import common.messages.KVMessage.StatusType;
import app_kvClient.KVClient;
import clientCommunication.ClientSocketListener;
import clientCommunication.ClientSocketListener.SocketStatus;

/**
 * @author Danielondon
 *
 */
public class CommunicationAsync implements ICommunication{

	private Socket myClientConnection;
	private InputStream myInputStream;
	private OutputStream myOutputStream;
	private static final int BUFFER_SIZE = 1024;
	private static final int DROP_SIZE = 1024 * BUFFER_SIZE;

	private Logger logger = LoggingManager.CreateClientLogger(CommunicationAsync.class);
	private Set<ClientSocketListener> listeners;
	private int port;
	private String ipAddress;
	private boolean isRunning;
	
	private MarshallerKvMessage marshaller;
	
	public CommunicationAsync(String port, String ipaddress) throws Exception{
		listeners = new HashSet<ClientSocketListener>();
		marshaller = new MarshallerKvMessage();	 
		this.port = -1;
		this.ipAddress = ipaddress;
		try {
			this.port = Integer.parseInt(port);
			this.myClientConnection = new Socket(this.ipAddress, this.port);
		}
		catch (NumberFormatException ex){
			throw new NumberFormatException("Error. Port is not in a correct format.");
		}
	}
	
	public void run() {
		try {
			this.isRunning = true;			
			this.myOutputStream = myClientConnection.getOutputStream();
			this.myInputStream = myClientConnection.getInputStream();
			
			while(this.isRunning) {
				try {
					logger.info("Receiving message in while isRunning ");
					KVMessage latestMsg = this.receiveMessage();
					logger.info("MEssage received, now notifyinf listeners ");
					for(ClientSocketListener listener : listeners) {
						logger.info("Notifying listener " + listener);
						listener.handleNewMessage(latestMsg);
					}
				} catch (IOException ioe) {
					if(this.isRunning) {
						logger.error("Connection lost!");
						try {
							tearDownConnection();
							for(ClientSocketListener listener : listeners) {
								listener.handleStatus(
										SocketStatus.CONNECTION_LOST);
							}
						} catch (IOException e) {
							logger.error("Unable to close connection!");
						}
					}
				} catch (Exception e) {
					logger.error(e);
				}				
			}
		} catch (IOException ioe) {
			logger.error("Connection could not be established!");
			
		} finally {
			if(this.isRunning) {
				disconnect();
			}
		}
	}
	
	public synchronized void disconnect() {
		logger.info("disconnecting ...");
		
		try {
			if (this.myClientConnection != null) {
				tearDownConnection();
				for(ClientSocketListener listener : listeners) {
					listener.handleStatus(SocketStatus.DISCONNECTED);
				}
			}
		} catch (IOException ioe) {
			logger.error("Unable to close connection!");
		}
	}
	
	private void tearDownConnection() throws IOException {
		this.isRunning = false;
		logger.info("tearing down connection ...");
		if (this.myClientConnection != null) {
			this.myInputStream.close();
			this.myOutputStream.close();
			this.myClientConnection.close();
			this.myClientConnection = null;
			logger.info("connection closed!");
		}
	}

	/**
	 * Sends a string message 
	 */
	@Override
	public void sendMessage(KVMessage message) throws Exception {
		// Sends Message
		if(this.myClientConnection != null && !this.myClientConnection.isClosed())
		{
			this.send(this.marshaller.getBytes(message));
		}
		else
		{
			throw new CommunicationCustomException("Error. There is no connection available or connection has been lost.");
		}
	}
	
	/**
	 * Receives a message.
	 */
	@Override
	public KVMessage receiveMessage() throws IOException {	
		byte[] bytes = this.receive();
		KVMessage message = this.marshaller.getKVMessage(bytes);
		return message;
	}	

	/**
	 * Sends a set of bytes through a stream.
	 * @return
	 * @throws IOException
	 */
	private byte[] receive() throws IOException{
		logger.info("Reading bytes");
		byte[] temp = new byte[1024];
		// Read bytes
		int bytesAvailable = this.myInputStream.read(temp); 		
		byte [] message = new byte [bytesAvailable];
		System.arraycopy(temp, 0, message, 0, bytesAvailable);
		return message;
	}
			
	/**
	 * Reads the bytes from the input stream.
	 * @param message
	 * @throws IOException
	 */
	private void send(byte[] message) throws Exception{
		// Reads bytes
		logger.info("Writting bytes");
		this.myOutputStream.write(message);
		this.myOutputStream.flush();
	}
	
	@Override
	public void addListener(ClientSocketListener listener){
		listeners.add(listener);
	}

}
